/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package compiler.grammar;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author meemo
 */
public class Production
{
    //~ Static fields/initializers ---------------------------------------------

    private static int NEXT_ID = 0;

    //~ Instance fields --------------------------------------------------------

    private final ArrayList<Symbol> body = new ArrayList<Symbol>();
    private final ArrayList<String> earlyBody = new ArrayList<String>();
    private final String            name;
    private final int               id = nextId();
    private NonTerminal             nonTerminal;
    private Set<Terminal>           firstSet = new HashSet<Terminal>();
    private boolean                 nullable = false;
    private boolean                 resolved = false;

    //~ Constructors -----------------------------------------------------------

    public Production(String name)
    {
        this.name = name;
    }

    //~ Methods ----------------------------------------------------------------

    public static void reset()
    {
        NEXT_ID = 0;
    }

    public void add(String id)
    {
        earlyBody.add(id);
    }

    public void add(Symbol symbol)
    {
        earlyBody.add(symbol.getName());

        if (resolved) // should we put off the resolving till later ??
            body.add(symbol);
    }

    /**
     * @return true if changes happened
     */
    public boolean computeFirstSet()
    {
        Set<Terminal> firstSet = new HashSet<Terminal>();

        for (Symbol symb : body)
        {
            if (symb instanceof Terminal)
            {
                firstSet.add((Terminal)symb);

                break;
            }

            NonTerminal nt = (NonTerminal)symb;
            firstSet.addAll(nt.getFirstSet());

            if (!nt.isNullable())
                break;

            // we will NOT enter the loop again unless
            // all the previous symbols were nullable non terminals
        }

        if (this.firstSet.containsAll(firstSet))
            return false;

        this.firstSet.addAll(firstSet);

        return true;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj == null)
        {
            return false;
        }

        if (getClass() != obj.getClass())
        {
            return false;
        }

        final Production other = (Production)obj;

        if ((this.body != other.body) && ((this.body == null) || !this.body.equals(other.body)))
        {
            return false;
        }

        if (this.id != other.id)
        {
            return false;
        }

        return true;
    }

    public ArrayList<Symbol> getBody()
    {
        return body;
    }

    public Set<Terminal> getFirstSet()
    {
        return firstSet;
    }

    public int getId()
    {
        return id;
    }

    public String getName()
    {
        return name;
    }

    public NonTerminal getNonTerminal()
    {
        return this.nonTerminal;
    }

    @Override
    public int hashCode()
    {
        int hash = id;
        int i = 1;

        for (Symbol symb : getBody())
        {
            i += 1;
            hash *= (symb.hashCode() << i);
        }

        return hash;
    }

    public boolean resolveBody(Map<String, Symbol> symbols)
    {
        boolean error = false;

        for (String str : earlyBody)
        {
            if (!symbols.containsKey(str))
            {
                System.out.println("ERR: can't find ID " + str);
                error = true;
            }
            else
                body.add(symbols.get(str));
        }

        this.resolved = true; // for add(Symbol)

        return error;
    }

    @Override
    public String toString()
    {
        StringBuilder str = new StringBuilder();

        if (this.name != null)
            str.append(name);
        else
            str.append("S'");

        str.append(" = ");

        for (Symbol s : body)
            str.append(s.getName() + " ");

        return str.toString();
    }

    protected static synchronized int nextId()
    {
        return ++NEXT_ID; // starts with 1
    }

    /*default*/ void setNonTerminal(NonTerminal nonTerminal)
    {
        this.nonTerminal = nonTerminal;
    }

    /**
     * @return true if nullability changes
     */

    /*default*/
    boolean updateNullability()
    {
        if (isNullabe()) // nullability can't go from true to false
            return false;

        for (Symbol symb : body)
        {
            if (symb instanceof Terminal)
                return false; // if there is at least one terminal it can't be nullable

            NonTerminal nt = (NonTerminal)symb;

            if (!nt.isNullable())
                return false;
        }

        // we get here in 2 cases
        // 1- body is empty
        // 2- there is no terminals, and all the nonterminals are nullabe
        // in both cases this production is deemed to be nullable
        nullable = true;

        return isNullabe();
    }

    private boolean isNullabe()
    {
        return nullable;
    }
}
